package com.region.http.client.request.apcache;

import com.region.common.adapter.json.JSONAdapterFactory;
import com.region.common.adapter.logging.LoggerAdapter;
import com.region.common.adapter.logging.LoggerAdapterFactory;
import com.region.common.utils.RandomUtils;
import com.region.common.utils.ReflectUtils;
import com.region.http.client.common.ContentType;
import com.region.http.client.common.RequestType;
import com.region.http.client.config.RequestAnnotationConfig;
import com.region.http.client.exception.ResponseErrorException;
import com.region.http.client.model.MultiFileTransferMeta;
import com.region.http.client.model.RequestParameter;
import com.region.http.client.model.ResponseData;
import com.region.http.client.request.AbstractTemplateHttpExecutor;
import org.apache.http.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.*;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.EntityUtils;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * The abstract apache http client
 *
 * @author liujieyu
 * @date 2023/7/23 15:40
 * @description
 */
public abstract class AbstractApacheHttpExecutor extends AbstractTemplateHttpExecutor {

    private final LoggerAdapter loggerAdapter = LoggerAdapterFactory.getLogger(getClass());

    /**
     * Get Http Client
     *
     * @param url
     * @param config
     * @return
     * @throws UnsupportedOperationException
     */
    public abstract CloseableHttpClient getHttpClient(String url, RequestAnnotationConfig config) throws UnsupportedOperationException;

    /**
     * release http client
     *
     * @param httpClient
     * @throws UnsupportedOperationException
     */
    public abstract void releaseConnection(HttpClient httpClient) throws UnsupportedOperationException;

    @Override
    public ResponseData execute(String url, RequestAnnotationConfig requestAnnotationConfig, RequestParameter parameters) throws Exception {
        CloseableHttpClient httpClient = getHttpClient(url, requestAnnotationConfig);
        if (loggerAdapter.isDebugEnabled()) {
            loggerAdapter.debug("The request url is " + url);
            loggerAdapter.debug("The request config is " + requestAnnotationConfig);
        }
        HttpRequestBase httpRequest = getHttpRequest(url, requestAnnotationConfig, parameters);
        CloseableHttpResponse executeData;
        try {
            executeData = httpClient.execute(httpRequest);
            if (executeData.getStatusLine().getStatusCode() == 200) {
                if (loggerAdapter.isDebugEnabled()) {
                    loggerAdapter.debug("The url [" + url + "] execute success");
                }
                ResponseData responseData = ResponseData.getInstance();
                Header[] headers = executeData.getAllHeaders();
                if (headers != null) {
                    for (Header header : headers) {
                        responseData.addHeader(header.getName(), header.getValue());
                        HeaderElement[] elements = header.getElements();
                        for (HeaderElement element : elements) {
                            responseData.addNameValuePair(header.getName(), element.getName(), element.getValue());
                            NameValuePair[] nameValuePairs = element.getParameters();
                            for (NameValuePair nameValuePair : nameValuePairs) {
                                responseData.addNameValuePair(header.getName(), nameValuePair.getName(), nameValuePair.getValue());
                            }
                        }
                    }
                }
                responseData.setBody(EntityUtils.toByteArray(executeData.getEntity()));
                EntityUtils.consume(executeData.getEntity());
                return responseData;
            } else {
                String msg = "The apache client http execute error, status:" + executeData.getStatusLine();
                loggerAdapter.error(msg);
                throw new ResponseErrorException(msg);
            }
        } catch (Throwable throwable) {
            loggerAdapter.error("The request execute fail.", throwable);
            throw throwable;
        } finally {
            release(httpRequest, httpClient);
        }
    }

    /**
     * Generate apache request object based on request meta information
     *
     * @param url
     * @param requestAnnotationConfig
     * @param parameters
     * @return
     */
    public HttpRequestBase getHttpRequest(String url, RequestAnnotationConfig requestAnnotationConfig, RequestParameter parameters) {
        HttpRequestBase httpRequestBase = null;
        RequestType requestType = requestAnnotationConfig.getRequestType();
        if (requestAnnotationConfig.getContentType() == ContentType.X_WWW_FORM_URLENCODED) {
            // For application/x-www-form-urlencoded is encapsulating parameters to the request address.
            url = processUrl(parameters.getForm(), url);
        }
        switch (requestType) {
            case GET:
                httpRequestBase = new HttpGet(url);
                break;
            case POST:
                httpRequestBase = new HttpPost(url);
                // A put request is similar to a post request
            case PUT:
                if (httpRequestBase == null) {
                    httpRequestBase = new HttpPut(url);
                }
                if (requestAnnotationConfig.getContentType() == ContentType.MULTIPART_FORM_DATA_VALUE) {
                    processMultiPartForm(httpRequestBase, parameters);
                } else {
                    processBody(httpRequestBase, parameters.getBody(), requestAnnotationConfig.getContentType());
                }
                break;
            case DELETE:
                httpRequestBase = new HttpDelete(url);
                break;
            case TRACE:
                httpRequestBase = new HttpTrace(url);
                break;
            case HEAD:
                httpRequestBase = new HttpHead(url);
                break;
            case OPTIONS:
                httpRequestBase = new HttpOptions(url);
                break;
            default:
                ;
        }
        processHeader(httpRequestBase, parameters);
        return httpRequestBase;
    }

    /**
     * Processing request header parameters
     *
     * @param httpRequestBase
     * @param parameter
     */
    private void processHeader(HttpRequestBase httpRequestBase, RequestParameter parameter) {
        if (parameter.getHeaders() != null) {
            for (Map.Entry<String, Object> entry : parameter.getHeaders().entrySet()) {
                String headerName = entry.getKey();
                httpRequestBase.setHeader(headerName, String.valueOf(entry.getValue()));
            }
        }
    }

    /**
     * Processing body type
     *
     * @param httpRequestBase
     * @param body
     * @param contentType
     */
    private void processBody(HttpRequestBase httpRequestBase, Object body, ContentType contentType) {
        if (contentType.MULTIPART_FORM_DATA_VALUE == contentType) {
            Map<String, Object> objectMap = ReflectUtils.getKeyValueByObject(body, false);
            List<NameValuePair> list = new ArrayList<>();
            for (Map.Entry<String, Object> entry : objectMap.entrySet()) {
                BasicNameValuePair basicNameValuePair = new BasicNameValuePair(entry.getKey(), String.valueOf(entry.getValue()));
                list.add(basicNameValuePair);
            }
            try {
                if (list != null && !list.isEmpty()) {
                    ((HttpEntityEnclosingRequestBase) httpRequestBase).setEntity(new UrlEncodedFormEntity(list));
                }
            } catch (UnsupportedEncodingException e) {
                loggerAdapter.error("Unsupported encoding error info:" + e.getMessage(), e);
                throw new IllegalArgumentException("Unsupported encoding error info:" + e.getMessage());
            }
        } else {
            String bodyString;
            if (body instanceof String) {
                bodyString = String.valueOf(body);
            } else {
                bodyString = JSONAdapterFactory.getAdapter().toJSONString(body);
            }
            StringEntity bodyEntity = new StringEntity(bodyString, org.apache.http.entity.ContentType.create(contentType.getValue(), Consts.UTF_8));
            ((HttpEntityEnclosingRequestBase) httpRequestBase).setEntity(bodyEntity);
        }
    }

    /**
     * Processing form type request data
     *
     * @param httpRequestBase
     * @param parameter
     */
    private void processMultiPartForm(HttpRequestBase httpRequestBase, RequestParameter parameter) {
        HttpEntity httpEntity = null;
        if (existsFileType(parameter)) {
            // Submission using form entities
            MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
            // General Parameter Addition
            for (Map.Entry<String, Object> entry : parameter.getForm().entrySet()) {
                // parameter name
                String key = entry.getKey();
                // value
                Object value = entry.getValue();
                if (value instanceof File) {
                    multipartEntityBuilder.addBinaryBody(key, (File) value);
                } else if (value instanceof InputStream) {
                    multipartEntityBuilder.addBinaryBody(key, (InputStream) value, org.apache.http.entity.ContentType.DEFAULT_BINARY, RandomUtils.getId());
                } else if (value instanceof byte[]) {
                    multipartEntityBuilder.addBinaryBody(key, (byte[]) value, org.apache.http.entity.ContentType.DEFAULT_BINARY, RandomUtils.getId());
                } else if (value instanceof MultiFileTransferMeta) {
                    MultiFileTransferMeta transferMeta = (MultiFileTransferMeta) value;
                    // process multipart file
                    if (transferMeta.getParameters() != null) {
                        for (Map.Entry<String, Object> data : transferMeta.getParameters().entrySet()) {
                            String name = data.getKey();
                            Object fileValue = data.getValue();
                            if (fileValue instanceof byte[]) {
                                multipartEntityBuilder.addBinaryBody(key, (byte[]) fileValue, org.apache.http.entity.ContentType.DEFAULT_BINARY, name);
                            }
                            if (fileValue instanceof InputStream) {
                                multipartEntityBuilder.addBinaryBody(key, (InputStream) fileValue, org.apache.http.entity.ContentType.DEFAULT_BINARY, name);
                            }
                            if (fileValue instanceof File) {
                                multipartEntityBuilder.addBinaryBody(key, (File) fileValue);
                            }
                        }
                    }
                } else {
                    multipartEntityBuilder.addTextBody(key, String.valueOf(value),
                            org.apache.http.entity.ContentType.create("text/plain", Charset.forName("UTF-8")));
                }
            }
            // Preventing Garbled Codes
            multipartEntityBuilder.setCharset(StandardCharsets.UTF_8);
            multipartEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
            httpEntity = multipartEntityBuilder.build();
        } else {
            List<NameValuePair> list = new ArrayList<>();
            // handle form data
            if (parameter.getForm() != null) {
                for (Map.Entry<String, Object> entry : parameter.getForm().entrySet()) {
                    BasicNameValuePair basicNameValuePair = new BasicNameValuePair(entry.getKey(), String.valueOf(entry.getValue()));
                    list.add(basicNameValuePair);
                }
            }
            // handle body object
            if (parameter.getBody() != null) {
                Map<String, Object> data = ReflectUtils.getKeyValueByObject(parameter.getBody(), false);
                for (Map.Entry<String, Object> entry : data.entrySet()) {
                    BasicNameValuePair basicNameValuePair = new BasicNameValuePair(entry.getKey(), String.valueOf(entry.getValue()));
                    list.add(basicNameValuePair);
                }
            }
            try {
                httpEntity = new UrlEncodedFormEntity(list, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                loggerAdapter.error("Unsupported Encoding Error Info:" + e.getMessage(), e);
                throw new IllegalArgumentException("Unsupported Encoding Error Info:" + e.getMessage());
            }
        }
        if (httpEntity != null) {
            ((HttpEntityEnclosingRequestBase) httpRequestBase).setEntity(httpEntity);
        }
    }



    /**
     * Releasing a TCP connection
     *
     * @param httpRequestBase
     * @param httpClient
     */
    private void release(HttpRequestBase httpRequestBase, HttpClient httpClient) {
        httpRequestBase.releaseConnection();
        releaseConnection(httpClient);
    }

    /**
     * get ssl socket factory
     *
     * @return
     */
    protected SSLConnectionSocketFactory getSslSocketFactory() {
        SSLContext sslContext;
        try {
            sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
                @Override
                public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    return true;
                }
            }).build();
            HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
            SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
            return sslSocketFactory;
        } catch (Exception e) {
            loggerAdapter.error("get ssl socket connection factory error", e);
            return null;
        }
    }
}
